package com.neo.tiny.bpm.service.process.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.neo.tiny.bpm.dto.definition.BpmProcessDefinitionCreateReqDTO;
import com.neo.tiny.bpm.dto.model.BpmModelMetaInfoRespDTO;
import com.neo.tiny.bpm.entity.BpmFormDO;
import com.neo.tiny.bpm.enums.model.BpmModelFormTypeEnum;
import com.neo.tiny.bpm.service.process.BpmFormService;
import com.neo.tiny.bpm.service.process.BpmModelService;
import com.neo.tiny.bpm.service.process.BpmProcessDefinitionService;
import com.neo.tiny.bpm.service.process.BpmTaskAssignRuleService;
import com.neo.tiny.bpm.util.ValidationUtils;
import com.neo.tiny.bpm.vo.model.*;
import com.neo.tiny.common.common.ResResult;
import com.neo.tiny.common.constant.ErrorCodeConstants;
import com.neo.tiny.common.exception.WebApiException;
import com.neo.tiny.query.LambdaQueryWrapperBase;
import com.neo.tiny.util.PageUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.common.engine.impl.util.io.BytesStreamSource;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @Description:
 * @Author: yqz
 * @CreateDate: 2022/9/25 23:53
 */
@Slf4j
@Service
@AllArgsConstructor
public class BpmModelServiceImpl implements BpmModelService {

    private final RepositoryService repositoryService;

    private final BpmFormService bpmFormService;

    private final BpmProcessDefinitionService processDefinitionService;

    private final BpmTaskAssignRuleService taskAssignRuleService;

    @Override
    public ResResult<IPage<BpmModelPageItemRespVO>> modelPage(BpmModelPageReqVO vo) {

        ModelQuery modelQuery = repositoryService.createModelQuery();

        Optional.ofNullable(vo.getKey())
                .ifPresent(modelQuery::modelKey);

        Optional.ofNullable(vo.getName())
                .ifPresent(name -> modelQuery.modelNameLike("%" + vo.getName() + "%"));

        Optional.ofNullable(vo.getCategory())
                .ifPresent(modelQuery::modelCategory);

        // 开始查询
        List<Model> models = modelQuery.orderByCreateTime().desc()
                .listPage(PageUtils.getStart(vo), vo.getSize());

        Set<Long> formIds = models.stream().map(model -> {
            BpmModelMetaInfoRespDTO metaInfoRespDTO = JSONUtil.toBean(model.getMetaInfo(),
                    BpmModelMetaInfoRespDTO.class);
            return Optional.ofNullable(metaInfoRespDTO)
                    // 如果上一步判断不为空，则执行下一步
                    .map(BpmModelMetaInfoRespDTO::getFormId)
                    .orElse(null);
        }).collect(Collectors.toSet());

        Map<Long, BpmFormDO> formMap = bpmFormService.getFormMap(formIds);
        // 获得 Deployment Map
        Set<String> deploymentIds = new HashSet<>();

        models.forEach(model -> Optional.ofNullable(model.getDeploymentId())
                .ifPresent(deploymentIds::add));

        Map<String, Deployment> deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds);

        // 获得 ProcessDefinition Map
        List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);

        Map<String, ProcessDefinition> processDefinitionMap = processDefinitions.stream()
                .collect(Collectors.toMap(ProcessDefinition::getDeploymentId, Function.identity(), (key1, key2) -> key1));
        // 拼接结果
        long modelCount = modelQuery.count();
        List<BpmModelPageItemRespVO> modelPageItemRespList = convertModel(models, formMap, deploymentMap, processDefinitionMap);
        IPage<BpmModelPageItemRespVO> page = new Page<>();
        page.setRecords(modelPageItemRespList);
        page.setTotal(modelCount);
        page.setSize(vo.getSize());
        page.setCurrent(vo.getCurrent());
        page.setPages(modelCount / vo.getSize() + 1);
        return ResResult.success(page);
    }


    /**
     * 组装模型数据
     *
     * @param models               模型集合
     * @param formMap              表单集合
     * @param deploymentMap        流程部署 Map
     * @param processDefinitionMap 流程定义map
     * @return 分页数据
     */
    private List<BpmModelPageItemRespVO> convertModel(List<Model> models, Map<Long, BpmFormDO> formMap,
                                                      Map<String, Deployment> deploymentMap,
                                                      Map<String, ProcessDefinition> processDefinitionMap) {

        return models.stream().map(model -> {
            BpmModelMetaInfoRespDTO metaInfo = JSONUtil.toBean(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);

            BpmFormDO form = formMap.get(metaInfo.getFormId());
            Deployment deployment = deploymentMap.get(model.getDeploymentId());
            ProcessDefinition processDefinition = processDefinitionMap.get(model.getDeploymentId());

            BpmModelPageItemRespVO vo = new BpmModelPageItemRespVO();

            BeanUtil.copyProperties(model, vo);
            BeanUtil.copyProperties(metaInfo, vo);
            Optional.ofNullable(form).ifPresent(bpmForm -> {
                vo.setFormId(bpmForm.getId());
                vo.setFormName(bpmForm.getName());
            });
            // ProcessDefinition
            BpmModelPageItemRespVO.ProcessDefinition definition = null;
            if (!Objects.isNull(processDefinition)) {
                definition = new BpmModelPageItemRespVO.ProcessDefinition();
                definition.setId(processDefinition.getId());
                definition.setVersion(processDefinition.getVersion());
                definition.setSuspensionState(processDefinition.isSuspended() ?
                        SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode());
                definition.setDeploymentTime(deployment.getDeploymentTime());
            }
            vo.setProcessDefinition(definition);

            return vo;
        }).collect(Collectors.toList());
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createModel(BpmModelCreateReqVO modelVO, String bpmnXml) {

        checkKeyModelKey(modelVO.getKey());
        // 校验流程标识已经存在
        Model model = this.getModelByKey(modelVO.getKey());

        if (!Objects.isNull(model)) {
            throw new WebApiException(ErrorCodeConstants.MODEL_KEY_EXISTS);
        }
        // 创建流程定义
        Model newModel = repositoryService.newModel();
        BeanUtil.copyProperties(modelVO, newModel);
        BpmModelMetaInfoRespDTO metaInfo = new BpmModelMetaInfoRespDTO();

        Optional.ofNullable(modelVO.getDescription()).ifPresent(metaInfo::setDescription);
        newModel.setMetaInfo(JSONUtil.toJsonStr(metaInfo));

        // 更新模型
        repositoryService.saveModel(newModel);
        // 更新 BPMN XML
        saveModelBpmnXml(newModel, bpmnXml);
        return newModel.getId();
    }


    /**
     * 更新模型
     * 因为进行多个操作，所以开启事务
     *
     * @param updateReqVO 更新信息
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateModel(BpmModelUpdateReqVO updateReqVO) {
        // 校验模型是否存在
        Model model = repositoryService.getModel(updateReqVO.getId());
        Assert.notNull(model, ErrorCodeConstants.MODEL_NOT_EXISTS.getMsg());

        BeanUtil.copyProperties(updateReqVO, model);

        String metaInfo = model.getMetaInfo();
        if (StrUtil.isNotBlank(metaInfo)) {
            BpmModelMetaInfoRespDTO metaInfoRespDTO = JSONUtil.toBean(metaInfo, BpmModelMetaInfoRespDTO.class);
            BeanUtil.copyProperties(updateReqVO, metaInfoRespDTO);
            model.setMetaInfo(JSONUtil.toJsonStr(metaInfoRespDTO));
        }

        repositoryService.saveModel(model);
        // 更新 BPMN XML
        this.saveModelBpmnXml(model, updateReqVO.getBpmnXml());
    }

    @Override
    public BpmModelRespVO getModel(String modelId) {

        Model model = repositoryService.getModel(modelId);
        if (Objects.isNull(model)) {
            return null;
        }

        BpmModelRespVO bpmModelRespVO = BeanUtil.copyProperties(model, BpmModelRespVO.class);

        String metaInfo = model.getMetaInfo();
        Optional.ofNullable(metaInfo).ifPresent(info -> {
            BpmModelMetaInfoRespDTO metaInfoResp = JSONUtil.toBean(info, BpmModelMetaInfoRespDTO.class);
            BeanUtil.copyProperties(metaInfoResp, bpmModelRespVO);
        });

        byte[] editorSource = repositoryService.getModelEditorSource(modelId);
        bpmModelRespVO.setBpmnXml(StrUtil.utf8Str(editorSource));
        return bpmModelRespVO;
    }

    // 因为进行多个操作，所以开启事务
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deployModel(String modelId) {
        // 校验模型是否存在
        Model model = repositoryService.getModel(modelId);
        if (Objects.isNull(model)) {
            throw new WebApiException(ErrorCodeConstants.MODEL_NOT_EXISTS);
        }
        // 校验流程图
        byte[] bpmBytes = repositoryService.getModelEditorSource(modelId);
        if (Objects.isNull(bpmBytes)) {
            throw new WebApiException(ErrorCodeConstants.MODEL_NOT_EXISTS);
        }
        // TODO 校验流程图的有效性；例如说，是否有开始的元素，是否有结束的元素；
        // 校验表单已配
        BpmFormDO form = checkFormConfig(model.getMetaInfo());

        // 校验任务分配规则已配置
        taskAssignRuleService.checkTaskAssignRuleAllConfig(modelId);

        BpmProcessDefinitionCreateReqDTO definitionCreateReqDTO = convertProcess(model, form);
        definitionCreateReqDTO.setBpmnBytes(bpmBytes);
        if (StrUtil.isNotBlank(model.getMetaInfo())) {
            BpmModelMetaInfoRespDTO metaInfoRespDTO = JSONUtil.toBean(model.getMetaInfo(),
                    BpmModelMetaInfoRespDTO.class);
            definitionCreateReqDTO.setDescription(metaInfoRespDTO.getDescription());
        }
        //校验模型是否发生修改。如果未修改，则不允许创建
        if (processDefinitionService.isProcessDefinitionEquals(definitionCreateReqDTO)) {
            ProcessDefinition oldProcessInstance = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
            if (oldProcessInstance != null && taskAssignRuleService.isTaskAssignRulesEquals(model.getId(), oldProcessInstance.getId())) {
                throw new WebApiException(ErrorCodeConstants.MODEL_DEPLOY_FAIL_TASK_INFO_EQUALS);
            }
        }
        // 创建流程定义
        String definitionId = processDefinitionService.createProcessDefinition(definitionCreateReqDTO);
        // 将老的流程定义进行挂起。也就是说，只有最新部署的流程定义，才可以发起任务。
        updateProcessDefinitionSuspended(model.getDeploymentId());

        // 更新 model 的 deploymentId，进行关联
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId);
        model.setDeploymentId(definition.getDeploymentId());

        repositoryService.saveModel(model);

        //复制任务分配规则
        taskAssignRuleService.copyTaskAssignRules(modelId, definition.getId());

    }

    @Override
    public void updateModelState(String id, Integer state) {

        // 校验流程模型存在
        Model model = repositoryService.getModel(id);
        if (model == null) {
            throw new WebApiException(ErrorCodeConstants.MODEL_NOT_EXISTS);
        }
        // 校验流程定义存在
        ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
        if (definition == null) {
            throw new WebApiException(ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS);
        }
        // 更新状态
        processDefinitionService.updateProcessDefinitionState(definition.getId(), state);
    }

    @Override
    public void deleteModel(String id) {
        // 校验流程模型存在
        Model model = repositoryService.getModel(id);
        if (model == null) {
            throw new WebApiException(ErrorCodeConstants.MODEL_NOT_EXISTS);
        }
        // 执行删除
        repositoryService.deleteModel(id);
        // 禁用流程实例
        updateProcessDefinitionSuspended(model.getDeploymentId());
    }
    @Override
    public BpmnModel getBpmnByModelId(String modelId) {
        byte[] bpmnBytes = repositoryService.getModelEditorSource(modelId);
        if (ArrayUtil.isEmpty(bpmnBytes)) {
            return null;
        }
        BpmnXMLConverter converter = new BpmnXMLConverter();
        return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
    }

    /**
     * 挂起 deploymentId 对应的流程定义。 这里一个deploymentId 只关联一个流程定义
     *
     * @param deploymentId 流程发布Id.
     */
    private void updateProcessDefinitionSuspended(String deploymentId) {
        if (StrUtil.isEmpty(deploymentId)) {
            return;
        }
        ProcessDefinition oldDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId);
        if (oldDefinition == null) {
            return;
        }
        processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode());
    }


    /**
     * 转换model为dto
     *
     * @param model 流程模型
     * @param form  表单
     * @return 流程定义
     */
    private BpmProcessDefinitionCreateReqDTO convertProcess(Model model, BpmFormDO form) {
        BpmProcessDefinitionCreateReqDTO dto = new BpmProcessDefinitionCreateReqDTO();
        dto.setModelId(model.getId());
        dto.setName(model.getName());
        dto.setKey(model.getKey());
        dto.setCategory(model.getCategory());
        String metaInfo = model.getMetaInfo();
        if (StrUtil.isNotBlank(metaInfo)) {
            BpmModelMetaInfoRespDTO metaInfoRespDTO = JSONUtil.toBean(metaInfo, BpmModelMetaInfoRespDTO.class);
            BeanUtil.copyProperties(metaInfoRespDTO, dto);
        }
        Optional.ofNullable(form).ifPresent(formData -> {
            dto.setFormConf(formData.getConf());
            dto.setFormFields(formData.getFields());
        });
        return dto;
    }

    /**
     * 校验流程表单已配置
     *
     * @param metaInfoStr 流程模型 metaInfo 字段
     * @return 流程表单
     */
    private BpmFormDO checkFormConfig(String metaInfoStr) {
        BpmModelMetaInfoRespDTO metaInfo = JSONUtil.toBean(metaInfoStr, BpmModelMetaInfoRespDTO.class);
        if (metaInfo == null || metaInfo.getFormType() == null) {
            throw new WebApiException(ErrorCodeConstants.MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
        }
        // 校验表单存在
        if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.NORMAL.getType())) {
            BpmFormDO form = bpmFormService
                    .getOne(new LambdaQueryWrapperBase<BpmFormDO>().eq(BpmFormDO::getId, metaInfo.getFormId()));

            if (Objects.isNull(form)) {
                throw new WebApiException(ErrorCodeConstants.FORM_NOT_EXISTS);
            }
            return form;
        }
        return null;
    }

    /**
     * 校验模型key是否合法
     *
     * @param key 模型key
     */
    private void checkKeyModelKey(String key) {
        if (!ValidationUtils.isXmlName(key)) {
            throw new WebApiException(ErrorCodeConstants.MODEL_KEY_VALID);
        }
    }

    private Model getModelByKey(String key) {
        return repositoryService.createModelQuery().modelKey(key).singleResult();
    }


    private void saveModelBpmnXml(Model model, String bpmnXml) {
        if (StrUtil.isEmpty(bpmnXml)) {
            return;
        }
        repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
    }

}
